Labels for Switch Patterns
We frequently come across scenarios where we need to check multiple values before executing conditional code. These can lead to nested if
statements, which are potentially hard to read and maintain. switch
statements are a great alternative with multiple advantages: The compiler performs exhaustiveness checking, and powerful pattern matching is more natural than using the rather weird if case
syntax. Additionally, a switch
statement can be more efficient than chaining multiple conditionals, as the value to be switched over is read once only, unlike once for each if
condition check.
It is important to remember that the switch is evaluated in sequential order until a match is found. By strategically ordering the most specific cases at the top, and the more general catch-all cases at the bottom, we can implement some powerful logic in a simple and concise manner. In the following example, we add the most general case where the isAdmin
condition is irrelevant at the bottom:
switch (isLoggedIn, isAdmin) {
case (true, true):
// ...
case (true, false):
// ...
case (false, _):
// For logged-out users, we don't care whether they are an admin.
}
Great, this works! But let's face it, it's hard to read. Of course, we can figure out what "case (true, false):
" in the above example means, but we have to cross-reference the tuple at the top of the switch
statement.
Let's refactor to improve readability. A very powerful tool is to use bespoke types in place of nondescript true
/false
boolean values. Here is our example rewritten with two enum
types specific for each use case:
enum SessionState {
case loggedIn, loggedOut
}
enum Role {
case admin, user
}
// elsewhere:
switch (sessionState, role) {
case (.loggedIn, .admin):
// do admin stuff
case (.loggedIn, _):
// do non-admin stuff
case (.loggedOut, _):
// ask user to log in
Nice! Now our code is locally glanceable. Of course, it may be a bit overkill to do this everywhere. Before we look at a more lightweight approach, it's important to consider additional benefits of custom types. When using a boolean isAdmin
value, we could later accidentally replace it with some unrelated boolean value, such as isGuestUser
. The compiler is happy with this change, and suddenly all guest users are allowed to perform Admin tasks. Oops! With a bespoke Role
type, we get compiler errors preventing this bug. Additionally, a boolean value always has exactly two states, which makes it harder to introduce a new role for guest users. But it's easy with our Role
type: we just add a new .guest
case.
However, it may be overkill to define custom types if we only use them for a single switch. A more lightweight approach is to use labels instead. Let's see how they can provide similar readability benefits:
switch (loggedIn: isLoggedIn, isAdmin: isAdmin) {
case (loggedIn: true, isAdmin: true):
// do admin stuff
case (loggedIn: true, isAdmin: false):
// do non-admin stuff
case (loggedIn: false, _):
// ask user to log in
}
Notice how we don't have to write the label for the catch-all underscore pattern in the third case
. Of course, we can mix and match the two approaches as needed. In this example, I would probably prefer a custom Role
type, and use a label for a boolean loggedIn
state.